﻿/*
 * Copyright © 2011 Tristan Reeves, treeves@gmail.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Linq.Expressions;
using Sprinkles.ExpressionHandlers;
using Sprinkles.Secondary;

namespace Sprinkles
{
    public class Deployment<T> : IDeployment, IInternalDeployment
    {
        DeploymentInfo IDeployment.ExtractInfo()
        {
            var t = typeof(T);
            var allNames = new List<string>();

            while (t != null)
            {
                var nam = t.FullName;
                allNames.Add(nam);
                t = t.BaseType;
            }

            foreach (var ifc in typeof(T).GetInterfaces())
            {
                var nam = ifc.FullName;
                allNames.Add(nam);
            }

            string name;
            string[] aliases;
            var hasUserDefinedName = (m_UserDefinedName != "");

            if (hasUserDefinedName)
            {
                name = m_UserDefinedName;
                aliases = allNames.ToArray();
            }
            else
            {
                name = allNames.First();
                aliases = allNames.Skip(1).ToArray();
            }

            var result = new DeploymentInfo(name, aliases, hasUserDefinedName, typeof(T));
            result.IsSingleton = m_IsSingleton;
            result.IsLazyInit = m_IsLazyInit;
            result.InitMethod = m_InitMethod;
            result.DestroyMethod = m_DestroyMethod;
            result.Scope = m_Scope;
            result.DependsReferences = m_DependsReferences.ToArray();
            result.MethodOverrides = m_MethodOverrideInfos.ToArray();
            result.IsAbstract = m_IsAbstract;
            result.ParentNameFunc = m_ParentNameFunc;

            //defaults
            result.DefaultForTypes = m_DefaultForTypes.ToArray();

            //static factory
            result.StaticFactoryMethodName = m_StaticFactoryMethodName;
            result.StaticFactoryMethodType = m_StaticFactoryMethodType;

            //instance factory
            result.InstanceFactoryMethodNameFunc = m_InstanceFactoryMethodName;
            result.InstanceFactoryObjectName = m_InstanceFactoryObjectName;

            //callbacks
            result.PreinitCallbacks = m_PreinitCallbacks.ToArray();
            result.PostinitCallbacks = m_PostinitCallbacks.ToArray();

            //properties
            result.PropertyCombinations = m_PropertyCombinations.ToArray();

            //constructors, factories
            foreach (var arg in m_ConstructorArgs) result.AddConstructorArg(arg);
            return result;
        }
        bool IDeployment.IsFinished { get { return m_IsFinished; } }
        void IDeployment.ChangeNameIfNecessary(string newName)
        {
            m_UserDefinedName = (newName ?? "").Trim();
        }
        void IInternalDeployment.AddConstructorArg(Arg arg)
        {
            this.AddConstructorArg(arg);
        }

        private string m_StaticFactoryMethodName;
        private Type m_StaticFactoryMethodType;

        private string m_InstanceFactoryMethodName;
        private Func<string> m_InstanceFactoryObjectName;

        private Scope m_Scope = Scope.None;
        private string[] m_DependsReferences = new string[0];
        private string m_InitMethod;
        private string m_DestroyMethod;
        private bool m_IsFinished;
        private string m_UserDefinedName = "";
        private bool m_IsSingleton = true;
        private bool m_IsLazyInit;
        private readonly List<Type> m_DefaultForTypes = new List<Type>();
        private readonly List<Arg> m_ConstructorArgs = new List<Arg>();
        private readonly List<MethodOverrideInfo> m_MethodOverrideInfos = new List<MethodOverrideInfo>();
        private readonly List<Action<object>> m_PreinitCallbacks = new List<Action<object>>();
        private readonly List<Action<object>> m_PostinitCallbacks = new List<Action<object>>();
        private readonly List<KeyValuePair<PropertyInfo, object>> m_PropertyCombinations = new List<KeyValuePair<PropertyInfo, object>>();
        private bool m_IsAbstract;
        private Func<string> m_ParentNameFunc;
        private void ClearCreationalItems()
        {
            m_ConstructorArgs.Clear();
            m_StaticFactoryMethodName = null;
            m_StaticFactoryMethodType = null;
            m_InstanceFactoryMethodName = null;
            m_InstanceFactoryObjectName = null;
        }
        private string GetName()
        {
            return m_UserDefinedName;
        }

        internal void AddConstructorArg(Arg arg)
        {
            m_ConstructorArgs.Add(arg);
        }
        internal void AddPropertyValue(PropertyInfo info, object value)
        {
            m_PropertyCombinations.Add(new KeyValuePair<PropertyInfo, object>(info, value));
        }
        internal void SetInstanceFactoryDetails(string methodName, Func<string> referenceName)
        {
            m_InstanceFactoryMethodName = methodName;
            m_InstanceFactoryObjectName = referenceName;
        }
        internal void SetStaticFactoryDetails(Type declaringType, string methodName)
        {
            m_StaticFactoryMethodType = declaringType;
            m_StaticFactoryMethodName = methodName;
        }
        internal object ContainerContext { get; set; }
        internal Func<string, object> ObjectRetrievingCallback { get; set; }
        internal DeploymentHost DeploymentHost { get; set; }

        public FutureDeployedObject<T> Done()
        {
            return DoneWithTypeChange<T>();
        }
        public FutureDeployedObject<TProductType> DoneWithTypeChange<TProductType>()
        {
            m_IsFinished = true;
            var result = new FutureDeployedObject<TProductType>(GetName, ObjectRetrievingCallback);
            return result;
        }
        public Deployment<T> IsDefaultFor<TDefaultType>()
        {
            return IsDefaultFor(typeof(TDefaultType));
        }
        public Deployment<T> IsDefaultForDeclaredType()
        {
            return IsDefaultFor(typeof(T));
        }
        public Deployment<T> IsDefaultFor(Type defaultForType)
        {
            if (defaultForType == null) throw new ArgumentNullException("defaultForType");
            m_DefaultForTypes.Add(defaultForType);
            return this;
        }
        public Deployment<T> SetAlias(string alias)
        {
            DeploymentHost.SetAlias(alias, () => this.m_UserDefinedName);
            return this;
        }
        public Deployment<T> DependsOn(string reference, params string[] additionalReferences)
        {
            reference = (reference ?? "").Trim();
            m_DependsReferences = (new[] { reference }.Union((additionalReferences ?? new string[0])))
                .Select(x => (x ?? "").Trim())
                .Distinct()
                .ToArray();

            return this;
        }
        public Deployment<T> Abstract(bool isAbstract)
        {
            m_IsAbstract = isAbstract;
            return this;
        }
        public Deployment<T> Name(string name)
        {
            m_UserDefinedName = (name ?? "").Trim();
            return this;
        }
        public Deployment<T> ParentObjectReference(string parentName)
        {
            parentName = (parentName ?? "").Trim();
            m_ParentNameFunc = () => parentName;
            return this;
        }
        public Deployment<T> ParentObjectReference(IFutureDeployedObject parentReference)
        {
            m_ParentNameFunc = () => parentReference.GetName();
            return this;
        }
        public Deployment<T> Singleton(bool singleton)
        {
            m_IsSingleton = singleton;
            return this;
        }
        public Deployment<T> SetScope(Scope scope)
        {
            m_Scope = scope;
            return this;
        }
        public Deployment<T> LazyInit(bool lazy)
        {
            m_IsLazyInit = lazy;
            return this;
        }
        public Deployment<T> InitMethod(Expression<Action<T>> method)
        {
            m_InitMethod = null;

            var h = new MethodExpressionHandler<T>();
            h.Process(method);
            if (!h.IsValid) throw new InvalidOperationException("The method [" + method + "] is not a valid init-method.");
            if (h.IsStatic) throw new InvalidOperationException("The method [" + method + "] is not a valid init-method. Static methods are not allowed.");
            if (h.Arguments.Length != 0) throw new InvalidOperationException("The method [" + method + "] is not a valid init-method. Only parameterless methods are allowed.");
            m_InitMethod = h.MethodName;

            return this;
        }
        public Deployment<T> DestroyMethod(Expression<Action<T>> method)
        {
            m_DestroyMethod = null;

            var h = new MethodExpressionHandler<T>();
            h.Process(method);
            if (!h.IsValid) throw new InvalidOperationException("The method [" + method + "] is not a valid destroy-method.");
            if (h.IsStatic) throw new InvalidOperationException("The method [" + method + "] is not a valid destroy-method. Static methods are not allowed.");
            if (h.Arguments.Length != 0) throw new InvalidOperationException("The method [" + method + "] is not a valid destroy-method. Only parameterless methods are allowed.");
            m_DestroyMethod = h.MethodName;

            return this;
        }
        public Deployment<T> MethodOverride<TResult>(Expression<Func<T, TResult>> method, string objectReference)
        {
            var h = new MethodExpressionHandler<T>();
            h.Process(method);
            if (!h.IsValid) throw new InvalidOperationException("The method [" + method + "] is not a valid candidate for method overriding.");
            if (!h.IsVirtual) throw new InvalidOperationException("The method [" + method + "] is not a valid candidate for method overriding. It is not marked virtual or abstract.");
            if (h.IsStatic) throw new InvalidOperationException("The method [" + method + "] is not a valid candidate for method overriding. Static methods are not allowed.");
            if (h.Arguments.Length != 0) throw new InvalidOperationException("The method [" + method + "] is not a valid candidate for method overriding. Only parameterless methods are allowed.");

            var oi = new MethodOverrideInfo() { MethodName = h.MethodName, ObjectReference = () => objectReference };
            m_MethodOverrideInfos.Add(oi);
            return this;
        }
        public Deployment<T> MethodOverride<TResult>(Expression<Func<T, TResult>> method, FutureDeployedObject<TResult> objectReference)
        {
            var h = new MethodExpressionHandler<T>();
            h.Process(method);
            if (!h.IsValid) throw new InvalidOperationException("The method [" + method + "] is not a valid candidate for method overriding.");
            if (!h.IsVirtual) throw new InvalidOperationException("The method [" + method + "] is not a valid candidate for method overriding. It is not marked virtual or abstract.");
            if (h.IsStatic) throw new InvalidOperationException("The method [" + method + "] is not a valid candidate for method overriding. Static methods are not allowed.");
            if (h.Arguments.Length != 0) throw new InvalidOperationException("The method [" + method + "] is not a valid candidate for method overriding. Only parameterless methods are allowed.");

            var oi = new MethodOverrideInfo() { MethodName = h.MethodName, ObjectReference = objectReference.GetName };
            m_MethodOverrideInfos.Add(oi);
            return this;
        }
        public Deployment<T> MethodOverride(string methodName, string objectReference)
        {
            var oi = new MethodOverrideInfo() { MethodName = methodName, ObjectReference = () => objectReference };
            m_MethodOverrideInfos.Add(oi);
            return this;
        }
        public Deployment<T> MethodOverride(string methodName, IFutureDeployedObject objectReference)
        {
            var oi = new MethodOverrideInfo() { MethodName = methodName, ObjectReference = objectReference.GetName };
            m_MethodOverrideInfos.Add(oi);
            return this;
        }
        public Deployment<T> PreInitCallback(Action<T> callback)
        {
            m_PreinitCallbacks.Add(o => callback((T)o));
            return this;
        }
        public Deployment<T> PostInitCallback(Action<T> callback)
        {
            m_PostinitCallbacks.Add(o => callback((T)o));
            return this;
        }
        public PropertyValueSetter<TResult, T> Property<TResult>(Expression<Func<T, TResult>> propertyGetter)
        {
            var ph = new PropertyExpressionHandler<T>();
            ph.Process(propertyGetter, typeof(TResult));
            if (!ph.IsValid) throw new InvalidOperationException("The property [" + propertyGetter + "] is not a valid property expression.");

            var result = new PropertyValueSetter<TResult, T>(this, ph.PropertyInfo);
            return result;
        }
        public PropertyValueSetter<TResult, T> Property<TResult>(string propertyName)
        {
            var pi = typeof(T).GetProperty(propertyName, typeof(TResult));
            if (pi == null) throw new NullReferenceException("The given property [" + propertyName + "] cannot be located on type [" + typeof(T) + "]");

            var result = new PropertyValueSetter<TResult, T>(this, pi);
            return result;
        }
        public CreationDeployment<T> CreatedBy
        {
            get
            {
                ClearCreationalItems();
                return new CreationDeployment<T>(this);
            }
        }
    }
}
